/*
 * Copyright 2010 John Kozura
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.bfr.client.selection;

import com.bfr.client.selection.impl.RangeImpl;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.*;

import java.util.*;

/**
* Implements a text range in a Document, everything between two RangeEndPoints.
* Works with both a (browser dependent) javascript range object, and with
* the java RangeEndPoint objects, building one or the other as needed on
* demand.
* 
* @author John Kozura
*/
public class Range
{   
    // For use in compareBoundaryPoint, which end points to compare
    public static final short START_TO_START	= 0;
    public static final short START_TO_END	= 1;
    public static final short END_TO_END	= 2;
    public static final short END_TO_START	= 3;
    
    private static RangeImpl c_impl = 
	(RangeImpl)GWT.create(RangeImpl.class);

    /**
    * Returns the next adjacent text node in the given direction.  Will move
    * down the hierarchy, then through siblings, then up, looking for the first 
    * text node.
    *
    * This could be non-statically included in the Node class
    *
    * @param current An element to start the search from, can be any type
    *                of node.
    * @param forward whether to search forward or backward
    * @return the next (previous) text node, or null if no more
    */
    public static Text getAdjacentTextElement(Node current, boolean forward)
    {
	Text res = getAdjacentTextElement(current, null, forward, false);
	return res;
    }
    
    /**
    * Returns the next adjacent text node in the given direction.  Will move
    * down the hierarchy (if traversingUp is not set), then through siblings, 
    * then up (but not past topMostNode), looking for the first node
    *
    * This could be non-statically included in the Node class
    *
    * @param current An element to start the search from, can be any type
    *                of node.
    * @param topMostNode A node that this will traverse no higher than
    * @param forward whether to search forward or backward
    * @param traversingUp if true, will not look at the children of this element
    * @return the next (previous) text node, or null if no more
    */
    public static Text getAdjacentTextElement(Node current,
                                              Node topMostNode,
                                              boolean forward,
                                              boolean traversingUp)
    {
	Text res = null;
	Node node;
	
	// If traversingUp, then the children have already been processed
	if (!traversingUp)
	{
	    if (current.getChildCount() > 0)
	    {
		node = forward ? current.getFirstChild() 
			       : current.getLastChild();
		
		if (node.getNodeType() == Node.TEXT_NODE)
		{
		    res = (Text)node;
		}
		else
		{
		    // Depth first traversal, the recursive call deals with 
		    // siblings
		    res = getAdjacentTextElement(node, topMostNode,
		                                 forward, false);
		}
	    }
	}
	
	if (res == null)
	{
	    node = forward ? current.getNextSibling() 
		           : current.getPreviousSibling();
	    // Traverse siblings
	    if (node != null)
	    {
		if (node.getNodeType() == Node.TEXT_NODE)
		{
		    res = (Text)node;
		}
		else
		{
		    // Depth first traversal, the recursive call deals with 
		    // siblings
		    res = getAdjacentTextElement(node, topMostNode,
		                                 forward, false);
		}
	    }
	}
	
	// Go up and over if still not found
	if ((res == null) && (current != topMostNode))
	{
	    node = current.getParentNode();
	    // Stop at document (technically could stop at "html" tag)
	    if ((node != null) && (node.getNodeType() != Node.DOCUMENT_NODE))
	    {
		res = getAdjacentTextElement(node, topMostNode,
		                             forward, true);
	    }
	}
	
	return res;
    }
    
    /**
    * Returns all text nodes between (and including) two arbitrary text nodes.
    * Caller must ensure startNode comes before endNode.
    *
    * @param startNode start node to traverse
    * @param endNode end node to finish traversal
    * @return A list of all text nodes between these two text nodes
    */
    public static List<Text> getSelectedTextElements(Text startNode, 
                                                     Text endNode)
    {
	List<Text> res = new ArrayList<Text>();
		
	Text current = startNode;
	while ((current != null) && (current != endNode))
	{
	    res.add(current);
	    
	    current = getAdjacentTextElement(current, null, true, false);
	} 
	
	if (current == null)
	{
	    // With the old way this could have been backwards, but should not
	    // happen now, so this is an error
	    res = null;
	}
	else
	{
	    res.add(current);
	}
	return res;
    }
    
    // Starting point of this range
    private RangeEndPoint m_startPoint;
    
    // Ending point of this range
    private RangeEndPoint m_endPoint;
    
    // The document this range is contained within.
    private Document m_document;
    
    // The javascript rendering of this range
    private RangeImpl.JSRange m_range;
    
    // The points as they were last time m_range was computed
    private RangeEndPoint m_lastStartPoint;
    private RangeEndPoint m_lastEndPoint;
    
    /**
    * Creates an empty range on this document
    *
    * @param doc Document to create an empty range in
    */
    public Range(Document doc)
    {
	setDocument(doc);
    }
    
    /**
    * Creates a range that encompasses the given element
    *
    * @param element Element to create a range around
    */
    public Range(Element element)
    {
	setRange(element);
    }
    
    /**
    * Creates a range that is a cursor at the given location
    *
    * @param cursorPoint a single point to make a cursor range
    */
    public Range(RangeEndPoint cursorPoint)
    {
	setCursor(cursorPoint);
    }
    
    /**
    * Create a range that extends between the given points.  Caller must
    * ensure that end comes after start
    *
    * @param startPoint start point of the new range
    * @param endPoint end point of the new range
    */
    public Range(RangeEndPoint startPoint, RangeEndPoint endPoint) 
    {
	setRange(startPoint, endPoint);
    }
    
    /**
    * Internal method for creating a range from a JS object
    *
    * @param document
    * @param rangeObj
    */
    Range(Document document, RangeImpl.JSRange rangeObj)
    {
	m_document = document;
	m_range = rangeObj;
    }
    
    /**
    * Internal function for retrieving the range, external callers should NOT
    * USE THIS
    *
    * @return
    */
    public RangeImpl.JSRange _getJSRange()
    {
	return m_range;
    }
    
    /**
    * Internal call to set the range, which skips some checks and settings;
    * this SHOULD NOT be used externally.
    *
    * @param startPoint
    * @param endPoint
    */
    public void _setRange(RangeEndPoint startPoint, RangeEndPoint endPoint) 
    {
	m_document = startPoint == null ? null : 
	    	     startPoint.getNode().getOwnerDocument();
	m_startPoint = startPoint;
	m_endPoint = endPoint;
    }
    
    /**
    * Collapses the range into a cursor, either to the start or end point
    *
    * @param start if true, cursor is the start point, otherwise the end point
    */
    public void collapse(boolean start)
    {
	if (m_range != null)
	{
	    c_impl.collapse(m_range, start);
	    m_startPoint = null;
	}
	else if (start)
	{
	    m_endPoint = m_startPoint;
	}
	else
	{
	    m_startPoint = m_endPoint;
	}
    }
    
    /**
    * Compares an endpoint of this range with an endpoint in another range,
    * returning -1, 0, or 1 depending whether the comparison endpoint comes
    * before, at, or after this endpoint.  how is a constant determining which
    * endpoints to compare, for example Range.START_TO_START.
    *
    * @param compare Range to compare against this one.
    * @param how constant indicating which endpoints to compare
    * @return -1, 0, or 1 indicating relative order of the endpoints
    */
    public int compareBoundaryPoint(Range compare, short how)
    {
	ensureRange();
	compare.ensureRange();
	
	return c_impl.compareBoundaryPoint(m_range, getJSRange(), how);
    }
    
    /**
    * Make a copy of the contents of this range, into the given element.  All
    * tags required to make the range complete will be included
    *
    * @param copyInto an element to copy the contents into, ie 
    *                 DOM.createSpanElement()
    */
    public void copyContents(Element copyInto)
    {
	ensureRange();
	c_impl.copyContents(m_range, copyInto);
    }
    
    /**
    * Remove the contents of this range from the DOM.
    */
    public void deleteContents()
    {
	ensureRange();
	c_impl.deleteContents(m_range);
    }
    
    public boolean equals(Object obj)
    {
	boolean res = false;
	
	try
	{
	    Range cmp = (Range)obj;
	    
	    ensureEndPoints();
	    cmp.ensureEndPoints();
	    res = (cmp == this) ||
	      	  (m_startPoint.equals(cmp.getStartPoint()) &&
	    	   m_endPoint.equals(cmp.getEndPoint()));
	}
	catch (Exception ex) {}
	
	return res;
    }
    
    /**
    * Place the contents of this range into a new SPAN element, removing them
    * from the DOM.  All tags required to make the range complete will be 
    * included.  This does not preserve the element object ids of the contents.
    *
    * @return a new SPAN element unattached to the DOM, containing the range
    *         contents.
    */
    public Element extractContents()
    {
	Element res = m_document.createSpanElement();
	extractContents(res);
	return res;
    }
    
    /**
    * Place the contents of this range into the given element, removing them
    * from the DOM.  All tags required to make the range complete will be 
    * included.  This does not preserve the element object ids of the contents.
    *
    * @param copyInto an element to extract the contents into, ie 
    *                 DOM.createSpanElement()
    */
    public void extractContents(Element copyInto)
    {
	ensureRange();
	c_impl.extractContents(m_range, copyInto);
    }
    
    /**
    * Get the element that is the lowest common ancestor of both ends of the
    * range.  In other words, the smallest element that includes the range.
    *
    * @return the element that completely encompasses this range
    */
    public Element getCommonAncestor()
    {
	ensureRange();
	return c_impl.getCommonAncestor(m_range);
    }
    
    /**
    * Gets a single point of the cursor location if this is a cursor, otherwise
    * returns null.
    *
    * @return the single point if this is a cursor and not a selection
    */
    public RangeEndPoint getCursor()
    {
	return isCursor() ? m_startPoint : null;
    }
    
    /**
    * Get the DOM Document this range is within
    *
    * @return document this range is in
    */
    public Document getDocument()
    {
	return m_document;
    }
    
    /**
    * Get the end point of the range.  Not a copy, so changing this alters
    * the range.
    *
    * @return the end point object
    */
    public RangeEndPoint getEndPoint()
    {
	ensureEndPoints();
	return m_endPoint;
    }
    
    /**
    * Gets an HTML string represnting all elements enclosed by this range.
    *
    * @return An html string of this range
    */
    public String getHtmlText()
    {
	ensureRange();
	return c_impl.getHtmlText(m_range);
    }
    
    /**
    * Get the JS object representing this range.  Since it is highly browser
    * dependent, it is not recommended to operate on this
    *
    * @return JavaScriptObject representing this range
    */
    public RangeImpl.JSRange getJSRange()
    {
	ensureRange();
	return m_range;
    }
    
    /**
    * Returns a list of all text elements that are part of this range, in order.
    *
    * @return all elements in this range
    */
    public List<Text> getSelectedTextElements()
    {
	return getSelectedTextElements(m_startPoint.getTextNode(),
	                               m_endPoint.getTextNode());
    }
    
    /**
    * Get the start point of the range.  Not a copy, so changing this alters
    * the range.
    *
    * @return the start point object
    */
    public RangeEndPoint getStartPoint()
    {
	ensureEndPoints();
	return m_startPoint;
    }
    
    /**
    * Gets the plain text that is enclosed by this range
    *
    * @return A string of the text in this range
    */
    public String getText()
    {
	ensureRange();
	return c_impl.getText(m_range);
    }
    
    /**
    * Returns whether this is a cursor, ie the start and end point are equal
    *
    * @return true if start == end
    */
    public boolean isCursor()
    {
	ensureEndPoints();
	return m_startPoint.equals(m_endPoint);
    }
    
    /**
    * Minimize the number of text nodes included in this range.  If the start
    * point is at the end of a text node, move it to the beginning of the
    * next text node; vice versa for the end point.  The result should ensure
    * no text nodes with 0 included characters.
    */
    public void minimizeTextNodes()
    {
	ensureEndPoints();
	m_startPoint.minimizeBoundaryTextNodes(true);
	m_endPoint.minimizeBoundaryTextNodes(false);
    }
    
    /**
    * TODO NOT IMPLEMENTED YET
    * Move the end points to encompass a boundary type, such as a word.
    * 
    * @param topMostNode a Node not to traverse above, or null
    * @param type unit to move boundary by, such as RangeEndPoint.MOVE_WORD
    */
    public void moveToBoundary(Node topMostNode, short type)
    {
	ensureEndPoints();
	m_startPoint.move(false, topMostNode, null, type, 1);
	m_endPoint.move(true, topMostNode, null, type, 1);
    }
    
    /**
    * Sets the range to a point cursor.
    * 
    * @param cursorPoint A single endpoint to create a cursor range at
    */
    public void setCursor(RangeEndPoint cursorPoint)
    {
	setRange(cursorPoint, cursorPoint);
    }
    
    /**
    * Sets just the end point of the range.  New endPoint must reside within
    * the same document as the current startpoint, and must occur after it.
    *
    * @param startPoint New start point for this range
    */
    public void setEndPoint(RangeEndPoint endPoint)
    {
	assert ((m_startPoint != null) ||
	        (endPoint.getNode().getOwnerDocument() == m_document));
	m_endPoint = endPoint;
	m_range = null;
    }
    
    /**
    * Sets the range to encompass the given element.  May not work around
    * non-text containing elements.
    *
    * @param element Element to surround by this range
    * @return whether a range can be placed around this element.
     */
    public boolean setRange(Element element)
    {
	Text firstText = getAdjacentTextElement(element, element, true, false);
	Text lastText = getAdjacentTextElement(element, element, false, false);
	
	if ((firstText == null) || (lastText == null))
	{
	    return false;
	}
	
	setRange(new RangeEndPoint(firstText, 0), 
	         new RangeEndPoint(lastText, lastText.getLength()));
	
	return true;
    }
    
    /**
    * Set the range to be between the two given points.  Both points must be
    * within the same document, and end must come after start.
    *
    * @param startPoint Start point to set the range to
    * @param endPoint End point to set the range to
     */
    public void setRange(RangeEndPoint startPoint, RangeEndPoint endPoint) 
    {
	assert (startPoint.getNode().getOwnerDocument() ==
	    endPoint.getNode().getOwnerDocument());
	
	_setRange(startPoint, endPoint);
	m_range = null;
    }
    
    /**
    * Sets just the start point of the range.  New startPoint must reside within
    * the same document as the current endpoint, and must occur before it.
    *
    * @param startPoint New start point for this range
    */
    public void setStartPoint(RangeEndPoint startPoint)
    {
	assert ((m_endPoint != null) &&
	        (startPoint.getNode().getOwnerDocument() == m_document));
	
	m_startPoint = startPoint;
	m_range = null;
    }
    
    /**
    * Surround all of the contents of the range with a new SPAN element, which
    * replaces the content in the DOM.  All tags required to make the range
    * complete are included in the child content.  This does not preserve the
    * element object ids of the contents.  The range will surround the new
    * element after this operation.
    *
    * @return The new span element that now surround the contents
    */
    public Element surroundContents()
    {
	Element res = m_document.createSpanElement();
	surroundContents(res);
	return res;
    }
    
    /**
    * Surround all of the contents of the range with the given element, which
    * replaces the content in the DOM.  All tags required to make the range
    * complete are included in the child content.  This does not preserve the
    * element object ids of the contents.  The range will surround this
    * element after this operation.
    *
    * @param copyInto an element to place the contents into, which will replace
    *                 them in the DOM after this operation
    */
    public void surroundContents(Element copyInto)
    {
	ensureRange();
	c_impl.surroundContents(m_range, copyInto);
	setRange(copyInto);
    }
    
    /**
    * Ensure the end points exists and are consisent with the javascript range
    */
    void ensureEndPoints()
    {
	if ((m_startPoint == null) || (m_endPoint == null))
	{
	    c_impl.fillRangePoints(this);
	    setupLastEndpoints();
	}
    }
    
    /**
    * Ensure the javascript range exists and is consistent with the end points
    */
    void ensureRange()
    {
	if (rangeNeedsUpdate())
	{
	    m_range = c_impl.createRange(m_document,
	                                 m_startPoint.getTextNode(), 
	                                 m_startPoint.getOffset(), 
	                                 m_endPoint.getTextNode(), 
	                                 m_endPoint.getOffset());
	    setupLastEndpoints();
	}
    }
    
    private boolean rangeNeedsUpdate()
    {
	return (m_range == null) ||
	       ((m_startPoint != null) &&
	        ((m_lastStartPoint == null) ||
	         !m_lastStartPoint.equals(m_startPoint) ||
	         (m_lastEndPoint == null) ||
	         !m_lastEndPoint.equals(m_endPoint)));
    }
    
    private void setupLastEndpoints()
    {
	m_lastStartPoint = new RangeEndPoint(m_startPoint);
	m_lastEndPoint = new RangeEndPoint(m_endPoint);
    }
    
    /**
    * Set the document this range is contained within
    * 
    * @param doc document to set
    */
    private void setDocument(Document doc)
    {
	if (m_document != doc)
	{
	    m_document = doc;
	    m_range = c_impl.createFromDocument(doc);
	}
    }
}
